<!DOCTYPE html>
<!--
Copyright 2016 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->

<link rel="import" href="/tracing/base/utils.html">
<link rel="import" href="/tracing/value/histogram.html">
<link rel="import" href="/tracing/value/histogram_deserializer.html">
<link rel="import" href="/tracing/value/histogram_grouping.html">

<script>
'use strict';

tr.exportTo('tr.v', function() {
  class HistogramSet {
    constructor(opt_histograms) {
      this.histograms_ = new Set();
      this.sharedDiagnosticsByGuid_ = new Map();

      if (opt_histograms !== undefined) {
        for (const hist of opt_histograms) {
          this.addHistogram(hist);
        }
      }
    }

    has(hist) {
      return this.histograms_.has(hist);
    }

    /**
     * Create a Histogram, configure it, add samples to it, and add it to this
     * HistogramSet.
     *
     * |samples| can be either
     *  0. a number, or
     *  1. a dictionary {value: number, diagnostics: dictionary}, or
     *  2. an array of
     *     2a. number, or
     *     2b. dictionaries {value, diagnostics}.
     *
     * @param {string} name
     * @param {!tr.b.Unit} unit
     * @param {number|!Object|!Array.<(number|!Object)>} samples
     * @param {!Object=} opt_options
     * @param {!tr.v.HistogramBinBoundaries} opt_options.binBoundaries
     * @param {!Object|!Map} opt_options.summaryOptions
     * @param {!Object|!Map} opt_options.diagnostics
     * @param {string} opt_options.description
     * @return {!tr.v.Histogram}
     */
    createHistogram(name, unit, samples, opt_options) {
      const hist = tr.v.Histogram.create(name, unit, samples, opt_options);
      this.addHistogram(hist);
      return hist;
    }

    /**
     * @param {!tr.v.Histogram} hist
     * @param {(!Object|!tr.v.d.DiagnosticMap)=} opt_diagnostics
     */
    addHistogram(hist, opt_diagnostics) {
      if (this.has(hist)) {
        throw new Error('Cannot add same Histogram twice');
      }

      if (opt_diagnostics !== undefined) {
        if (!(opt_diagnostics instanceof Map)) {
          opt_diagnostics = Object.entries(opt_diagnostics);
        }
        for (const [name, diagnostic] of opt_diagnostics) {
          hist.diagnostics.set(name, diagnostic);
        }
      }

      this.histograms_.add(hist);
    }

    /**
     * Add a Diagnostic to all Histograms so that it will only be serialized
     * once per HistogramSet rather than once per Histogram that contains it.
     *
     * @param {string} name
     * @param {!tr.v.d.Diagnostic} diagnostic
     */
    addSharedDiagnosticToAllHistograms(name, diagnostic) {
      this.addSharedDiagnostic(diagnostic);
      for (const hist of this) {
        hist.diagnostics.set(name, diagnostic);
      }
    }

    /**
     * Add a Diagnostic to this HistogramSet so that it will only be serialized
     * once per HistogramSet rather than once per Histogram that contains it.
     *
     * @param {!tr.v.d.Diagnostic} diagnostic
     */
    addSharedDiagnostic(diagnostic) {
      this.sharedDiagnosticsByGuid_.set(diagnostic.guid, diagnostic);
    }

    get length() {
      return this.histograms_.size;
    }

    * [Symbol.iterator]() {
      for (const hist of this.histograms_) {
        yield hist;
      }
    }

    /**
     * Filters Histograms by matching their name exactly.
     *
     * @param {string} name Histogram name.
     * @return {!Array.<!tr.v.Histogram>}
     */
    getHistogramsNamed(name) {
      return [...this].filter(h => h.name === name);
    }

    /**
     * Filters to find the Histogram that matches the specified name exactly.
     * If no Histogram with that name exists, undefined is returned. If multiple
     * Histograms with the name exist, an error is thrown.
     *
     * @param {string} name Histogram name.
     * @return {tr.v.Histogram}
     */
    getHistogramNamed(name) {
      const histograms = this.getHistogramsNamed(name);
      if (histograms.length === 0) return undefined;
      if (histograms.length > 1) {
        throw new Error(
            `Unexpectedly found multiple histograms named "${name}"`);
      }

      return histograms[0];
    }

    /**
     * Lookup a Diagnostic by its guid.
     *
     * @param {string} guid
     * @return {!tr.v.d.Diagnostic|undefined}
     */
    lookupDiagnostic(guid) {
      return this.sharedDiagnosticsByGuid_.get(guid);
    }

    deserialize(data) {
      for (const hist of tr.v.HistogramDeserializer.deserialize(data)) {
        this.addHistogram(hist);
      }
    }

    /**
     * Convert dicts to either Histograms or shared Diagnostics.
     *
     * @param {!Object} dicts
     */
    importDicts(dicts) {
      // The new HistogramSet JSON format is an array of at least 3 arrays.
      if ((dicts instanceof Array) &&
          (dicts.length > 2) &&
          (dicts[0] instanceof Array)) {
        this.deserialize(dicts);
        return;
      }

      // The original HistogramSet JSON format was a flat array of objects.
      for (const dict of dicts) {
        this.importLegacyDict(dict);
      }
    }

    /**
     * Convert dict to either a Histogram or a shared Diagnostic.
     *
     * @param {!Object} dict
     */
    importLegacyDict(dict) {
      if (dict.type !== undefined) {
        // TODO(benjhayden): Forget about TagMaps in 2019Q2.
        if (dict.type === 'TagMap') return;

        if (!tr.v.d.Diagnostic.findTypeInfoWithName(dict.type)) {
          throw new Error('Unrecognized shared diagnostic type ' + dict.type);
        }
        this.sharedDiagnosticsByGuid_.set(dict.guid,
            tr.v.d.Diagnostic.fromDict(dict));
      } else {
        const hist = tr.v.Histogram.fromDict(dict);
        this.addHistogram(hist);
        hist.diagnostics.resolveSharedDiagnostics(this, true);
      }
    }

    /**
     * Serialize all of the Histograms and shared Diagnostics to an Array of
     * dictionaries.
     *
     * @return {!Array.<!Object>}
     */
    asDicts() {
      const dicts = [];
      for (const diagnostic of this.sharedDiagnosticsByGuid_.values()) {
        dicts.push(diagnostic.asDict());
      }
      for (const hist of this) {
        dicts.push(hist.asDict());
      }
      return dicts;
    }

    /**
     * Find the Histograms whose names are not contained in any other
     * Histograms' RelatedNameMap diagnostics.
     *
     * @return {!Array.<!tr.v.Histogram>}
     */
    get sourceHistograms() {
      const diagnosticNames = new Set();
      for (const hist of this) {
        for (const diagnostic of hist.diagnostics.values()) {
          if (!(diagnostic instanceof tr.v.d.RelatedNameMap)) continue;
          for (const name of diagnostic.values()) {
            diagnosticNames.add(name);
          }
        }
      }

      const sourceHistograms = new HistogramSet;
      for (const hist of this) {
        if (!diagnosticNames.has(hist.name)) {
          sourceHistograms.addHistogram(hist);
        }
      }
      return sourceHistograms;
    }

    /**
     * Return a nested Map, whose keys are strings and leaf values are Arrays of
     * Histograms.
     * See GROUPINGS for example |groupings|.
     * Groupings are skipped when |opt_skipGroupingCallback| is specified and
     * returns true.
     *
     * @typedef {!Array.<tr.v.Histogram>} HistogramArray
     * @typedef {!Map.<string,!(HistogramArray|HistogramArrayMap)>}
     *   HistogramArrayMap
     * @typedef {!Map.<string,!HistogramArray>} LeafHistogramArrayMap
     *
     * @param {!Array.<!tr.v.HistogramGrouping>} groupings
     * @param {!function(!Grouping, !LeafHistogramArrayMap):boolean=}
     *   opt_skipGroupingCallback
     *
     * @return {!(HistogramArray|HistogramArrayMap)}
     */
    groupHistogramsRecursively(groupings, opt_skipGroupingCallback) {
      function recurse(histograms, level) {
        if (level === groupings.length) {
          return histograms;  // recursion base case
        }

        const grouping = groupings[level];
        const groupedHistograms = tr.b.groupIntoMap(
            histograms, grouping.callback);

        if (opt_skipGroupingCallback && opt_skipGroupingCallback(
            grouping, groupedHistograms)) {
          return recurse(histograms, level + 1);
        }

        for (const [key, group] of groupedHistograms) {
          groupedHistograms.set(key, recurse(group, level + 1));
        }

        return groupedHistograms;
      }

      return recurse([...this], 0);
    }

    /*
     * Histograms and Diagnostics are merged two at a time, without considering
     * any others, so it is possible for two merged Diagnostics to be equivalent
     * but not identical, which is inefficient. This method replaces equivalent
     * Diagnostics with shared Diagnostics so that the HistogramSet can be
     * serialized more efficiently and so that these Diagnostics can be compared
     * quickly when merging relationship Diagnostics.
     */
    deduplicateDiagnostics() {
      const namesToCandidates = new Map();  // string: Set<Diagnostic>
      const diagnosticsToHistograms = new Map();  // Diagnostic: [Histogram]
      const keysToDiagnostics = new Map();  // string: Diagnostic

      for (const hist of this) {
        for (const [name, candidate] of hist.diagnostics) {
          // TODO(#3695): Remove this check once equality is smoke-tested.
          if (candidate.equals === undefined) {
            this.sharedDiagnosticsByGuid_.set(candidate.guid, candidate);
            continue;
          }

          const hashKey = candidate.hashKey;
          if (candidate.hashKey !== undefined) {
            // TODO(857283): Fall back to slow path if same name but diff type
            if (keysToDiagnostics.has(hashKey)) {
              hist.diagnostics.set(name, keysToDiagnostics.get(hashKey));
            } else {
              keysToDiagnostics.set(hashKey, candidate);
              this.sharedDiagnosticsByGuid_.set(candidate.guid, candidate);
            }

            continue;
          }

          if (diagnosticsToHistograms.get(candidate) === undefined) {
            diagnosticsToHistograms.set(candidate, [hist]);
          } else {
            diagnosticsToHistograms.get(candidate).push(hist);
          }

          if (!namesToCandidates.has(name)) {
            namesToCandidates.set(name, new Set());
          }
          namesToCandidates.get(name).add(candidate);
        }
      }

      for (const [name, candidates] of namesToCandidates) {
        const deduplicatedDiagnostics = new Set();

        for (const candidate of candidates) {
          let found = false;
          for (const test of deduplicatedDiagnostics) {
            if (candidate.equals(test)) {
              const hists = diagnosticsToHistograms.get(candidate);
              for (const hist of hists) {
                hist.diagnostics.set(name, test);
              }
              found = true;
              break;
            }
          }
          if (!found) {
            deduplicatedDiagnostics.add(candidate);
          }

          for (const diagnostic of deduplicatedDiagnostics) {
            this.sharedDiagnosticsByGuid_.set(diagnostic.guid, diagnostic);
          }
        }
      }
    }

    /**
     * @param {!Iterable.<string>} names of GenericSet diagnostics
     * @return {!Array.<!tr.v.HistogramGrouping>}
     */
    buildGroupingsFromTags(names) {
      const tags = new Map();  // name: Set<string>
      for (const hist of this) {
        for (const name of names) {
          if (!hist.diagnostics.has(name)) continue;
          if (!tags.has(name)) tags.set(name, new Set());
          for (const tag of hist.diagnostics.get(name)) {
            tags.get(name).add(tag);
          }
        }
      }

      const groupings = [];
      for (const [name, values] of tags) {
        const built = tr.v.HistogramGrouping.buildFromTags(values, name);
        for (const grouping of built) {
          groupings.push(grouping);
        }
      }
      return groupings;
    }
  }

  return {HistogramSet};
});
</script>
